/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.aljfce.business.forecast.control;

import com.aljfce.business.entity.Calendar;
import com.aljfce.business.entity.Event;
import com.aljfce.business.entity.Notification;
import com.aljfce.business.entity.User;
import com.aljfce.business.entity.WeatherInformation;
import com.aljfce.business.entity.WeatherInformationPK;
import com.aljfce.business.forecast.exception.DayOutOfRangeException;
import com.aljfce.business.forecast.exception.ForecastNotFoundException;
import com.aljfce.business.forecast.control.openweathermap.WeatherProvider;
import com.aljfce.business.forecast.control.openweathermap.jaxb.Forecast;
import com.aljfce.business.forecast.control.openweathermap.jaxb.Weatherdata;
import com.aljfce.business.mail.control.MailManager;
import com.aljfce.business.notification.control.NotificationManager;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import javax.xml.bind.JAXBException;
import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.Days;

/**
 *
 * @author Jacopo Ferrigno <jacopo.ferrigno@gmail.com>
 */
@Stateless
public class ForecastManager {

    @PersistenceContext
    EntityManager em;

    @Inject
    Logger logger;

    @Inject
    MailManager mailManager;

    @Inject
    NotificationManager notificationManager;

    WeatherProvider wp;

    //String constans for accessing weather Api
    private final static String API_KEY = "f838f38bc7d05a5d974922ac35a2e4d9";
    private final static String OPENWEATHERURL = "http://api.openweathermap.org/data/2.5/";
    //MAx days
    /**
     * This constant represent the max time in the future for which is possible
     * to retrieve the forecast. 
     * 
     */
    public final static int MAXSPAN = 16;

    @PostConstruct
    void init() {
        this.wp = new WeatherProvider();
        this.wp.setApyKey(API_KEY);
        try {
            this.wp.setOpenWeatherUrl(new URL(OPENWEATHERURL));
        } catch (MalformedURLException ex) {
            logger.log(Level.SEVERE, "Someting wrong with the creation fo openweather url: {0}", ex);
        }
        this.wp.setFormat(WeatherProvider.FORMAT_XML);
        this.wp.setUnits(WeatherProvider.UNIT_METRIC);
    }

    /**
     * This method will search for weather information for a specific location
     * on a specific date. An exception is thrown if the parameter are not
     * valid.
     *
     * @param location for which search the forecast information.
     * @param date on which search the forecast information
     * @return a {@link WeatherInformation} object if the forecast is found
     * @throws DayOutOfRangeException if the date is in the future more than
     * {@link ForecastManager#MAXSPAN} days
     * @throws IllegalArgumentException if any of the argument is null
     * @throws ForecastNotFoundException if the forecast is not found.
     */
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public WeatherInformation getForecast(String location, Date date) throws DayOutOfRangeException, IllegalArgumentException, ForecastNotFoundException {
        if (date == null) {
            throw new IllegalArgumentException("Forecast date cannot be null");
        }

        DateTime forecastDate = new DateTime(date);
        DateTime now = new DateTime();
        //date cannot be in the past
        if (DateTimeComparator.getDateOnlyInstance().compare(forecastDate, now) == -1) {
            throw new IllegalArgumentException("Forecast date cannot be in the past");
        }

        //DaysBetween do not consider the ending intervall
        //So we need to add 1 do cont the las day
        int spanTime = Days.daysBetween(now, forecastDate).getDays() + 1;

        //If the dayis too mush in the future openWeatherMap will not have a record.
        //So throw an exception
        if (spanTime > ForecastManager.MAXSPAN) {
            throw new DayOutOfRangeException("The date spantime " + spanTime
                    + " exced the mas spanTime of "
                    + ForecastManager.MAXSPAN);
        }

        //Get All the forecast
        ArrayList<WeatherInformation> weatherList = this.getForecastList(location);
        //And extract the one we need.
        WeatherInformation weatherInformation = null;

        for (WeatherInformation wi : weatherList) {
            DateTime wDate = new DateTime(wi.getWeatherInformationPK().getDayTime());
            //Check it the date of the forecast is the same of the one we need.
            //In this case set weaterInformation to it and break to avoid wasting 
            //time
            if (DateTimeComparator.getDateOnlyInstance().compare(forecastDate, wDate) == 0) {
                weatherInformation = wi;
                //Not elegan but should work
                break;
            }

        }
        //Since the meteoservice will return a date without hour, minutes and second
        //Set the one in weatherInformation to the one passed as argument
        //TODO decide whats to do with dare in forecast
        weatherInformation.getWeatherInformationPK().setDayTime(date);

        return weatherInformation;
    }



    /**
     * This method will try to update and existing {@link WeatherInformation}
     * object. It will throw Exception in case of malformed input or error in
     * retrieving the weather informations. 
     * This method will not modify the passed object but will return a new one.
     *
     *
     * @param forecast the forecast object to update
     * @return a new {@link WeatherInformation} object with fields updated.
     * @throws IllegalArgumentException if the arguments or any important field is null
     * @throws DayOutOfRangeException if the date is in the future more than
     * {@link ForecastManager#MAXSPAN} days
     * @throws ForecastNotFoundException if the forecast is not found.
     */
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public WeatherInformation updateForecast(final WeatherInformation forecast) throws IllegalArgumentException, DayOutOfRangeException, ForecastNotFoundException {

        if (forecast == null) {
            throw new IllegalArgumentException("forecast object cannot be null");
        }
        if (forecast.getWeatherInformationPK() == null) {
            throw new IllegalArgumentException("Invalid WeatherInformationPK");
        }
        if(forecast.getWeatherInformationPK().getDayTime() == null ||
           forecast.getWeatherInformationPK().getLocation() == null){
            throw new IllegalArgumentException("Invalid filed on prymary key");
        }
        WeatherInformationPK wiPk = forecast.getWeatherInformationPK();
        /**
         * Since is the same logic as to get e new Forecast lets reuse the
         * function and update the existing forecast object. Most of the check
         * are made by getForecast, so they are only re-thrown.
         */
        WeatherInformation wi = this.getForecast(wiPk.getLocation(), wiPk.getDayTime());
        //If we returned from getForecast the object wi must be well populated.
        //So we don't neet to make checks
        //Build a new object and populate it
        
        WeatherInformation forecastUpdated = new WeatherInformation();
        WeatherInformationPK forecastPk = new WeatherInformationPK();
        //Build the primary key
        forecastPk.setDayTime(new Date(wiPk.getDayTime().getTime()));
        forecastPk.setLocation(wiPk.getLocation());
        forecastUpdated.setWeatherInformationPK(forecastPk);
        //set       
        forecastUpdated.setDetails(wi.getDetails());
        forecastUpdated.setUrlImage(wi.getUrlImage());
        forecastUpdated.setConditionCode(wi.getConditionCode());

        return forecastUpdated;
    }

    /**
     * This method will delete the {@link WeatherInformation} passed as argument,
     * if it is present in the persistence context. Otherwise it will throw an
     * exception.
     *
     * @param forecast the {@link WeatherInformation} object to remove
     * @throws IllegalArgumentException if the object passed is null or any of its important fields. 
     * @throws ForecastNotFoundException if the {@link  WeatherInformation} object passed as argument is not present in the persistence context
     */
    public void deleteForecast(WeatherInformation forecast) throws IllegalArgumentException, ForecastNotFoundException {
        if (forecast == null) {
            throw new IllegalArgumentException("Forecast entity cannot be null");
        }
        if (forecast.getWeatherInformationPK() == null) {
            throw new IllegalArgumentException("Invalid WeatherInformationPK");
        }
        if(forecast.getWeatherInformationPK().getDayTime() == null ||
           forecast.getWeatherInformationPK().getLocation() == null){
            throw new IllegalArgumentException("Invalid filed on prymary key");
        }
        
        WeatherInformation wi = em.find(WeatherInformation.class, forecast.getWeatherInformationPK());
        if (wi == null) {
            throw new ForecastNotFoundException();
        }
        em.remove(wi);
        em.flush();
    }

    @Schedule(second = "*", minute = "0", hour = "*/6", persistent = false)
    public void updateAll() {
        TypedQuery<WeatherInformation> query = em.createNamedQuery("WeatherInformation.findAll", WeatherInformation.class);
        List<WeatherInformation> allWeatherInformation = query.getResultList();
        for (WeatherInformation wi : allWeatherInformation) {
            //Save wheater information to make a comparison later.
            String weatherConditions = wi.getDetails();
            
            //If the day is in the past an exception should be catch 
            try {

                wi = this.updateForecast(wi);
                em.merge(wi);
                /**
                 * When the weather conditions for an event change, send a mail
                 * to ALL the participants And add a notification
                 */
                if (!weatherConditions.equals(wi.getDetails())) {
                    //Take all the event linked to this particular weatherInformation
                    for (Event event : wi.getEventCollection()) {
                        //Take all the Users that have this information in they calendar
                        //and sent them a mail.
                        //And add a notification
                        for (Calendar calendar : event.getCalendarCollection()) {
                            User user = calendar.getUser();
                            this.mailManager.sendWeatherChangedMsg(user, event, wi);
                            this.notificationManager.newNotification(Notification.TYPE.WEATHER_CHANGED, user, event);

                        }
                    }
                }

            } catch (IllegalArgumentException | DayOutOfRangeException | ForecastNotFoundException ex) {
                //do nothing
            }
        }
    }

    @Schedule(second = "*", minute = "0", hour = "3", persistent = false)
    public void checkWeatherCondition() {
        TypedQuery<WeatherInformation> query = em.createNamedQuery("WeatherInformation.findAll", WeatherInformation.class);
        DateTime oneDayFromNow = DateTime.now().plusDays(1);
        DateTime threeDayFromNow = DateTime.now().plusDays(3);

        for (WeatherInformation forecast : query.getResultList()) {
            //Check if event is tomorrow
            DateTime forecastDate = new DateTime(forecast.getWeatherInformationPK().getDayTime());
            DateTimeComparator dateCompare = DateTimeComparator.getDateOnlyInstance();
            //If one event tomorrow has bad weather conditions send message
            //ONE DAY BEFORE CHECK
            if (dateCompare.compare(forecastDate, oneDayFromNow) == 0
                    && isBadWeather(forecast)) {
                for (Event event : forecast.getEventCollection()) {
                    for (Calendar userCalendar : event.getCalendarCollection()) {
                        User user = userCalendar.getUser();
                        this.mailManager.send1DayNotificationMsg(user, event, forecast);
                        this.notificationManager.newNotification(Notification.TYPE.ONE_DAY_NOTIFICATION, user, event);
                    }
                }
            } //If an event in the next 3 days has bad weather conditions
            //Send the creator a notification
            //Three DAY BEFORE
            else if (dateCompare.compare(forecastDate, threeDayFromNow) == 0
                    && isBadWeather(forecast)) {
                try {
                    //Get all the wather information for the location
                    //And search the next sunny day
                                         
                    Date nextSunnyDay = null;
                    ArrayList<WeatherInformation> forecastList = this.getForecastList(forecast.getWeatherInformationPK().getLocation());
                    for(WeatherInformation wi: forecastList){
                        if(!isBadWeather(wi)){
                            nextSunnyDay = wi.getWeatherInformationPK().getDayTime();
                            /*
                             * To break is a way to ensure that the FIRST
                             * good day is chosen. To avoid it we should reverse
                             * the list (waste) of use a for cycle with a strange
                             * exit condition(really?)
                             */
                            break;
                        }
                    }
                    for (Event event : forecast.getEventCollection()) {
                        User creator = event.getCreator();
                        this.mailManager.send3DayNotificationMsg(creator, event, forecast);
                        this.notificationManager.newNotification(Notification.TYPE.THREE_DAY_NOTIFICATION, creator, event, nextSunnyDay , null);
                    }

                } catch (ForecastNotFoundException | DayOutOfRangeException ex) {
                    Logger.getLogger(ForecastManager.class.getName()).log(Level.SEVERE, null, ex);
                }

            }
        }

    }

    private Forecast getForecastOnDay(Weatherdata wd, DateTime forecastDate) {

        if (wd == null || wd.getForecasts() == null) {
            return null;
        }

        for (Forecast forecast : wd.getForecasts()) {
            if (!forecast.hasDay()) {
                return null;
            }

            int difference = Days.daysBetween(new DateTime(forecast.getDay()), forecastDate).getDays();

            if (difference == 0) {
                return forecast;
            }
        }
        return null;
    }
    
    
        @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    private ArrayList<WeatherInformation> getForecastList(String location) throws ForecastNotFoundException, DayOutOfRangeException {
        //Check that the event an the used fileds are not null
        if (location == null || location.isEmpty()) {
            throw new IllegalArgumentException("Location cannot be null or empty");
        }

        ArrayList<WeatherInformation> weatherList = new ArrayList<>();
        Weatherdata wd = null;

        try {
            wd = this.wp.getDailyForecast(location, ForecastManager.MAXSPAN);
        } catch (MalformedURLException ex) {
            logger.log(Level.SEVERE, "Someting wrong with the creation fo openweather url: {0}", ex);
        } catch (JAXBException ex) {
            logger.log(Level.SEVERE, "Someting went wrong with the XML classes: {0}", ex);
        }

        if (wd == null) {
            throw new ForecastNotFoundException("No forecast present for location specified");
        }

        //Build the arraylist of weather informations
        for (Forecast forecast : wd.getForecasts()) {
            if (forecast == null || !forecast.hasSymbol()) {
                throw new ForecastNotFoundException("Unable to extract the forecast form list");
            }
            //Finally build the WeaterInformation Object
            WeatherInformation wi = new WeatherInformation(forecast.getDay(), location);
            //Set the weather details
            wi.setDetails(forecast.getSymbol().getDescription());
            //If the url is null something gone wrong. Throw an exception
            if(forecast.getSymbol().getIcon() ==null)
                throw new ForecastNotFoundException("Unable to extract the forecast form list");
            wi.setUrlImage(this.buildUrl(forecast.getSymbol().getIcon()));
            //If the condition code is null something gone wrong. Throw an exception
            if(forecast.getSymbol().getConditionCode() == null)
                throw new ForecastNotFoundException("Unable to extract the forecast form list");
            wi.setConditionCode(forecast.getSymbol().getConditionCode());

            weatherList.add(wi);

        }

        return weatherList;
    }
    
   
    private String buildUrl(String icon) {
        String url = null;
        String baseUrl = "http://openweathermap.org/img/w/";
        String suffix = ".png";

        url = baseUrl + icon + suffix;

        return url;
    }

    private boolean isBadWeather(WeatherInformation weather) {
        int conditionCode = weather.getConditionCode();
        //If is in the 800 rangeis not bad
        if (conditionCode / 100 == 8) {
            return false;
        }
        if (conditionCode > 900 && conditionCode < 956) {
            return false;
        }
        return true;
    }
}
